use std::fs::DirEntry;

use indexmap::IndexSet;

use crate::helpers::messages::Messages;

#[derive(Debug, Clone, Default)]
pub struct Extensions {
    allowed_extensions_hashset: IndexSet<String>,
    excluded_extensions_hashset: IndexSet<String>,
}

impl Extensions {
    pub fn new() -> Self {
        Default::default()
    }

    pub(crate) fn filter_extensions(mut file_extensions: String) -> (IndexSet<String>, Messages) {
        let mut messages = Messages::new();
        let mut extensions_hashset = IndexSet::new();

        if file_extensions.trim().is_empty() {
            return (Default::default(), messages);
        }
        file_extensions = file_extensions.replace("IMAGE", "jpg,kra,gif,png,bmp,tiff,hdr,svg");
        file_extensions = file_extensions.replace("VIDEO", "mp4,flv,mkv,webm,vob,ogv,gifv,avi,mov,wmv,mpg,m4v,m4p,mpeg,3gp,mts,m2ts");
        file_extensions = file_extensions.replace("MUSIC", "mp3,flac,ogg,tta,wma,webm");
        file_extensions = file_extensions.replace("TEXT", "txt,doc,docx,odt,rtf");

        let extensions: Vec<String> = file_extensions.split(',').map(str::trim).map(String::from).collect();
        for mut extension in extensions {
            if extension.is_empty() || extension.replace(['.', ' '], "").trim().is_empty() {
                continue;
            }

            if extension.starts_with('.') {
                extension = extension.chars().skip(1).collect::<String>();
            }

            if extension.contains('.') {
                messages.warnings.push(format!("{extension} is not valid extension because contains dot inside"));
                continue;
            }

            if extension.contains(' ') {
                messages.warnings.push(format!("{extension} is not valid extension because contains empty space inside"));
                continue;
            }

            extensions_hashset.insert(extension);
        }
        (extensions_hashset, messages)
    }

    /// List of allowed extensions, only files with this extensions will be checking if are duplicates
    /// After, extensions cannot contain any dot, commas etc.
    pub(crate) fn set_allowed_extensions(&mut self, allowed_extensions: String) -> Messages {
        let (extensions, messages) = Self::filter_extensions(allowed_extensions);

        self.allowed_extensions_hashset = extensions;
        messages
    }

    pub(crate) fn set_excluded_extensions(&mut self, excluded_extensions: String) -> Messages {
        let (extensions, messages) = Self::filter_extensions(excluded_extensions);

        self.excluded_extensions_hashset = extensions;
        messages
    }

    #[expect(clippy::string_slice)] // Valid, because we address go to dot, which is known ascii character
    pub(crate) fn check_if_entry_have_valid_extension(&self, entry_data: &DirEntry) -> bool {
        if self.allowed_extensions_hashset.is_empty() && self.excluded_extensions_hashset.is_empty() {
            return true;
        }

        // Using entry_data.path().extension() is a lot of slower, even 5 times
        let file_name = entry_data.file_name();
        let Some(file_name_str) = file_name.to_str() else { return false };
        let Some(extension_idx) = file_name_str.rfind('.') else { return false };
        let extension = &file_name_str[extension_idx + 1..];

        if !self.allowed_extensions_hashset.is_empty() {
            if extension.chars().all(|c| c.is_ascii_lowercase()) {
                self.allowed_extensions_hashset.contains(extension)
            } else {
                self.allowed_extensions_hashset.contains(&extension.to_lowercase())
            }
        } else if extension.chars().all(|c| c.is_ascii_lowercase()) {
            !self.excluded_extensions_hashset.contains(extension)
        } else {
            !self.excluded_extensions_hashset.contains(&extension.to_lowercase())
        }
    }

    pub(crate) fn set_any_extensions(&self) -> bool {
        !self.allowed_extensions_hashset.is_empty()
    }

    fn extend_allowed_extensions(&mut self, file_extensions: &[&str]) {
        for extension in file_extensions {
            let extension_without_dot = extension.trim_start_matches('.');
            self.allowed_extensions_hashset.insert(extension_without_dot.to_string());
        }
    }

    // E.g. when using similar videos, user can provide extensions like "mp4,flv", but if user provide "mp4,jpg" then
    // it will be only "mp4" because "jpg" is not valid extension for videos

    fn union_allowed_extensions(&mut self, file_extensions: &[&str]) {
        let mut new_extensions = IndexSet::new();
        for extension in file_extensions {
            let extension_without_dot = extension.trim_start_matches('.');
            if !self.allowed_extensions_hashset.contains(extension_without_dot) {
                new_extensions.insert(extension_without_dot.to_string());
            }
        }
        self.allowed_extensions_hashset = new_extensions;
    }

    pub(crate) fn set_and_validate_allowed_extensions(&mut self, file_extensions: &[&str]) {
        if self.allowed_extensions_hashset.is_empty() {
            self.extend_allowed_extensions(file_extensions);
        } else {
            self.union_allowed_extensions(file_extensions);
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use super::*;

    #[test]
    fn test_filter_extensions_basic_and_replacements() {
        // Empty string
        let (exts, msgs) = Extensions::filter_extensions("".to_string());
        assert!(exts.is_empty());
        assert!(msgs.messages.is_empty() && msgs.warnings.is_empty() && msgs.errors.is_empty());

        // Basic extensions
        let (exts, msgs) = Extensions::filter_extensions("jpg,png,gif".to_string());
        assert_eq!(exts.len(), 3);
        assert!(exts.contains("jpg") && exts.contains("png") && exts.contains("gif"));
        assert!(msgs.warnings.is_empty());

        // With dots
        let (exts, _) = Extensions::filter_extensions(".jpg,.png".to_string());
        assert_eq!(exts.len(), 2);
        assert!(exts.contains("jpg") && exts.contains("png"));

        // IMAGE replacement
        let (exts, _) = Extensions::filter_extensions("IMAGE".to_string());
        assert!(exts.contains("jpg") && exts.contains("png") && exts.contains("bmp"));

        // VIDEO replacement
        let (exts, _) = Extensions::filter_extensions("VIDEO".to_string());
        assert!(exts.contains("mp4") && exts.contains("mkv") && exts.contains("avi"));

        // Invalid extensions with dot inside
        let (exts, msgs) = Extensions::filter_extensions("jpg,test.bad,png".to_string());
        assert_eq!(exts.len(), 2);
        assert!(!exts.contains("test.bad"));
        assert!(msgs.warnings.iter().any(|w| w.contains("test.bad")));

        // Invalid extensions with space
        let (exts, msgs) = Extensions::filter_extensions("jpg,bad ext,png".to_string());
        assert!(!exts.contains("bad ext"));
        assert!(msgs.warnings.iter().any(|w| w.contains("bad ext")));
    }

    #[test]
    fn test_set_allowed_and_excluded_extensions() {
        let mut ext = Extensions::new();

        let msgs = ext.set_allowed_extensions("jpg,png".to_string());
        assert!(msgs.warnings.is_empty());
        assert!(ext.set_any_extensions());

        let mut ext2 = Extensions::new();
        ext2.set_excluded_extensions("gif,bmp".to_string());
        assert!(!ext2.set_any_extensions());
    }

    #[test]
    fn test_check_if_entry_have_valid_extension() {
        let temp_dir = tempfile::tempdir().unwrap();
        let file_jpg = temp_dir.path().join("test.jpg");
        let file_png = temp_dir.path().join("test.PNG");
        let file_gif = temp_dir.path().join("test.gif");
        let file_txt = temp_dir.path().join("test.txt");
        let file_no_ext = temp_dir.path().join("noext");

        fs::write(&file_jpg, "test").unwrap();
        fs::write(&file_png, "test").unwrap();
        fs::write(&file_gif, "test").unwrap();
        fs::write(&file_txt, "test").unwrap();
        fs::write(&file_no_ext, "test").unwrap();

        // No extensions set - all should pass
        let ext = Extensions::new();
        assert!(ext.check_if_entry_have_valid_extension(&fs::read_dir(&temp_dir).unwrap().find(|e| e.as_ref().unwrap().file_name() == "test.jpg").unwrap().unwrap()));

        // Allowed extensions
        let mut ext = Extensions::new();
        ext.set_allowed_extensions("jpg,png".to_string());
        let entries: Vec<_> = fs::read_dir(&temp_dir).unwrap().map(|e| e.unwrap()).collect();
        assert!(ext.check_if_entry_have_valid_extension(entries.iter().find(|e| e.file_name() == "test.jpg").unwrap()));
        assert!(ext.check_if_entry_have_valid_extension(entries.iter().find(|e| e.file_name() == "test.PNG").unwrap())); // case insensitive
        assert!(!ext.check_if_entry_have_valid_extension(entries.iter().find(|e| e.file_name() == "test.gif").unwrap()));
        assert!(!ext.check_if_entry_have_valid_extension(entries.iter().find(|e| e.file_name() == "noext").unwrap()));

        // Excluded extensions
        let mut ext = Extensions::new();
        ext.set_excluded_extensions("txt".to_string());
        assert!(ext.check_if_entry_have_valid_extension(entries.iter().find(|e| e.file_name() == "test.jpg").unwrap()));
        assert!(!ext.check_if_entry_have_valid_extension(entries.iter().find(|e| e.file_name() == "test.txt").unwrap()));
    }

    #[test]
    fn test_set_and_validate_allowed_extensions() {
        let mut ext = Extensions::new();
        ext.set_and_validate_allowed_extensions(&["mp4", "mkv"]);
        assert_eq!(ext.allowed_extensions_hashset.len(), 2);
        assert!(ext.allowed_extensions_hashset.contains("mp4"));

        let mut ext = Extensions::new();
        ext.set_allowed_extensions("jpg,png,mp4".to_string());
        ext.set_and_validate_allowed_extensions(&["mp4", "mkv"]);
        assert!(ext.allowed_extensions_hashset.contains("mkv"));
        assert!(!ext.allowed_extensions_hashset.contains("jpg"));
    }
}
